feat: embed Coder agent chat in VS Code sidebar panel#844
feat: embed Coder agent chat in VS Code sidebar panel#844
Conversation
Add a Coder Chat sidebar that opens via deep link: vscode://coder.coder-remote/openChat?url=...&token=...&agentId=... The chat panel embeds /agents/:agentId/embed in an iframe through a local reverse proxy (needed to work around VS Code webview sandbox restrictions). Auth is handled via postMessage — the iframe signals readiness, the extension sends the session token, and the iframe sets it as an axios header for all API requests. - chatPanelProvider.ts: WebviewViewProvider with EmbedProxy and postMessage relay for auth bootstrap - uriHandler.ts: /openChat route that reads agentId and opens the panel - extension.ts: registers the provider and wires it into the URI handler - package.json: coderChat view container (panel area, no activity bar icon) with coder.chatPanel webview view
The local reverse proxy was added to work around VS Code's webview sandbox blocking script execution in nested cross-origin iframes. Testing confirms the sandbox restriction does not apply when the iframe loads directly from the Coder server URL, so the proxy is unnecessary complexity. Auth continues to flow via postMessage+setSessionToken (no cookies, no CSRF, no proxy header injection).
- Remove unused getErrorHtml() (dead after proxy removal). - Extract renderView() to deduplicate resolveWebviewView/refresh. - Fix package.json indentation (extra nesting introduced in prior commit).
The existing /open deep link now optionally accepts an agentId query parameter. When present, the chat panel opens alongside the workspace. Old extensions silently ignore unknown query params, so this is fully backwards compatible — no error dialog, no version negotiation needed. The standalone /openChat route is kept for cases where no workspace context is needed (e.g. direct links from the agents page).
a22e2c5 to
1e19564
Compare
| const iframe = document.getElementById('chat-frame'); | ||
| const status = document.getElementById('status'); | ||
|
|
||
| iframe.addEventListener('load', () => { | ||
| iframe.style.display = 'block'; | ||
| status.style.display = 'none'; | ||
| }); |
There was a problem hiding this comment.
This could be made more flexible so that even if messages arrive mid-run it wouldn't cause the status to always be displayed and never cleared (we could have some state tracking in this webview possibly). Anyway if this is unlikely or too much work then we can skip
There was a problem hiding this comment.
I think that's a great idea for a follow-up, but it's not needed for this now.
There was a problem hiding this comment.
This ties with the auth comment above, so maybe they should be tackled at the same time when needed!
There was a problem hiding this comment.
Sure, though I'd argue that it's less a matter of when it's needed and more a matter of if it's needed.
src/uri/uriHandler.ts
Outdated
| const routes: Readonly<Record<string, UriRouteHandler>> = { | ||
| "/open": handleOpen, | ||
| "/openDevContainer": handleOpenDevContainer, | ||
| "/openChat": handleOpenChat, |
There was a problem hiding this comment.
How can we trigger this one actually?
There was a problem hiding this comment.
Via a direct deep link
There was a problem hiding this comment.
I meant through the Coder UI, I could do /open but not /openChat, is it not implemented yet in coder/coder?
There was a problem hiding this comment.
The updated deep links are not yet part of coder/coder. I want to get the extension changes merged first, in case the deep link needs to change.
I'm happy to remove the /openChat and just leave /open for now.
| if (!this.agentId) { | ||
| webview.html = this.getNoAgentHtml(); | ||
| return; | ||
| } | ||
|
|
||
| const coderUrl = this.client.getHost(); | ||
| if (!coderUrl) { | ||
| webview.html = this.getNoAgentHtml(); | ||
| return; | ||
| } |
There was a problem hiding this comment.
I keep getting the no agent HTML even though I have a chat running, am I doing something wrong 🤔 ?
commands.open() triggers vscode.openFolder with a remote authority, which reloads the window and wipes in-memory state. To survive this, handleOpen now saves the agentId to globalState before the reload. After the reload, activate() reads and clears it once the deployment is configured, then opens the chat panel. This follows the same set-and-clear pattern used by firstConnect.
No reason for it to be in a separate ctx.subscriptions.push block — chatPanelProvider is already created above.
|
Addressed in 5c933a8 — replaced the Worth noting: there are zero code paths in this PR that can actually trigger this throw — both callers ( |
The /open route with an optional agentId param is the only deep link path for opening the chat panel. The standalone /openChat route and the chatPanelProvider dependency in the URI handler are no longer needed — the chat panel is opened from extension.ts after the remote-authority reload picks up the persisted agentId.
Summary
Adds a new Coder Chat webview panel that embeds the Coder agent chat UI directly inside VS Code, triggered via a deep link.
Deep link format
How it works
/openChatURI handler callssetupDeployment()to authenticate, then opens the chat panel.ChatPanelProviderrenders a webview containing an iframe pointing at/agents/{id}/embedon the Coder server.postMessage({ type: "coder:vscode-ready" })to the parent.{ type: "coder:vscode-auth-bootstrap", payload: { token } }into the iframe.API.setSessionToken(token), fetches user + permissions, and renders the authenticated chat UI.Why not cookies?
Cookie-based auth (
SameSite=None; Secure) does not work in VS Code webview iframes over HTTP. ThesetSessionTokenapproach uses header-based auth via axios, bypassing cookies and CSRF entirely.Files changed
package.json— AddedcoderChatview container (panel, not activity bar) andcoder.chatPanelwebview view.src/webviews/chat/chatPanelProvider.ts(new) —ChatPanelProviderclass: postMessage relay,openChat(agentId), iframe HTML generation.src/uri/uriHandler.ts— Added/openChatroute andhandleOpenChat()handler.src/extension.ts— Creates and registersChatPanelProvider, passes it to the URI handler.Testing
Verified end-to-end against
dev.coder.com: deep link → extension activation → postMessage auth → full chat UI rendered with multi-turn conversation and tool invocations.